﻿Imports System.IO
Imports System.Text

Public Class tServerCollection
    Inherits System.Collections.CollectionBase

#Region " Private Members "
    'internal list of servers
    Private ptServers As New List(Of tServer)
#End Region

#Region " Events "
    Public Event LogEvent(ByVal serverName As String, ByVal eventText As String, ByVal innerException As Exception, ByVal eventType As EventLogEntryType)
#End Region


#Region " Collection Implementations "
    Public Sub Add(ByVal aServer As tServer)
        List.Add(aServer)
    End Sub

    Public Sub Remove(ByVal index As Int32)
        If (index > Count) OrElse (index < 0) Then
            Throw New IndexOutOfRangeException
        Else
            List.RemoveAt(index)
        End If
    End Sub

    Public Sub Remove(ByVal name As String)
        Dim foundIndex As Int32 = -1
        Dim i As Int32 = 0

        For i = 0 To (Count - 1)
            If CType(List(i), tServer).Name = name Then
                foundIndex = i
                Exit For
            End If
        Next

        Me.Remove(foundIndex)
    End Sub

    Default Public ReadOnly Property Item(ByVal index As Int32) As tServer
        Get
            Return CType(List.Item(index), tServer)
        End Get
    End Property

    Default Public ReadOnly Property Item(ByVal name As String) As tServer
        Get
            Dim foundIndex As Int32 = -1
            Dim i As Int32 = 0

            For i = 0 To (Count - 1)
                If CType(List(i), tServer).Name = name Then
                    foundIndex = i
                    Exit For
                End If
            Next

            Return CType(List.Item(foundIndex), tServer)
        End Get
    End Property
#End Region

#Region " Custom Methods "

    ''' <summary>
    ''' Starts all servers using the supplied configuration
    ''' </summary>
    ''' <param name="config">Servers configuration collection, including it's location, config file, server commands, etc...</param>
    ''' <remarks></remarks>
    Public Sub StartAllServers(ByVal config As tConfiguration)
        Dim i As Int32 = 0

        For i = 0 To (config.Servers.Count - 1)
            Dim singleServer As New tServer

            Try
                'start server and add it to the list only if it was started sucessfully
                singleServer.StartServer(config.Servers(i))
                List.Add(singleServer)
            Catch ex As Exception
                'throw an event because the process failed to start
                Dim serverName As String = "*unknown*"

                If Not IsNothing(config.Servers(i)) Then
                    serverName = config.Servers(i).Name
                End If

                'raise an event to be handled in the caller
                RaiseEvent LogEvent(serverName, "Failed to start process!", ex, EventLogEntryType.Warning)
            End Try

        Next
    End Sub

    ''' <summary>
    ''' Stops all Terraria servers if they arent already running
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub StopAllServers()
        Dim i As Int32 = 0

        For i = 0 To (Count - 1)
            Dim singleServer As tServer = CType(List(i), tServer)

            'stop server if running
            If (Not IsNothing(singleServer)) AndAlso singleServer.IsServerRunning Then
                singleServer.StopServer()
            End If
        Next
    End Sub

    ''' <summary>
    ''' Broadcasts a message to all players currently online on all servers
    ''' </summary>
    ''' <param name="broadcastMessage">The text to be broadcasted</param>
    ''' <remarks></remarks>
    Public Sub BroadcastAllServers(ByVal broadcastMessage As String)
        Dim i As Int32 = 0

        For i = 0 To (Count - 1)
            CType(List(i), tServer).Broadcast(broadcastMessage)
        Next
    End Sub

    ''' <summary>
    ''' Sends a command to all Terraria servers
    ''' </summary>
    ''' <param name="commandText">text of the command itself</param>
    ''' <remarks></remarks>
    Public Sub SendCommandAllServers(ByVal commandText As String)
        Dim i As Int32 = 0

        For i = 0 To (Count - 1)
            CType(List(i), tServer).SendCommand(commandText)
        Next
    End Sub

    ''' <summary>
    ''' Immediately saves the world on all servers
    ''' </summary>
    ''' <remarks>Autosave ONLY if allowed. Some servers such as TShock have auto-save built-in and it would be redundant to autosave again.</remarks>
    Public Sub AutosaveAllServers()
        Dim i As Int32 = 0

        For i = 0 To (Count - 1)
            CType(List(i), tServer).AutoSave()
        Next
    End Sub


    ''' <summary>
    ''' TShock specfic command. Select Port on startup menu for all servers
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub selectTShockPortAllServers()
        Dim i As Int32 = 0

        For i = 0 To (Count - 1)
            CType(List(i), tServer).SelectTShockPort()
        Next
    End Sub

    ''' <summary>
    ''' TShock specfic command. Select World on startup menu for all servers
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub selectTShockWorldAllServers()
        Dim i As Int32 = 0

        For i = 0 To (Count - 1)
            CType(List(i), tServer).SelectTShockWorld()
        Next
    End Sub

#End Region


End Class


Public Class tServer

#Region " Constants "
    Private Const EX_COMMAND_SERVER_NOT_STARTED As String = "Server not stareted! Server must be running before attempting to send command '{0}'."

    Private Const FILE_ERROR_LOG_SUFFIX As String = ".error.log"
    Private Const FILE_CONSOLE_LOG_SUFFIX As String = ".console.log"
#End Region


#Region " Private Members "
    Private pServerProcess As Process
    Private pProcessStarted As Boolean = False
    Private pConsoleOutputBuffer As New StringBuilder("")
    Private pErrorBuffer As New StringBuilder("")
    Private pBufferErrors As Boolean = False
    Private pBufferConsoleOutput As Boolean = False
    Private pConsoleOutputStream As StreamWriter
    Private pErrorStream As StreamWriter
    Private pErrorCount As Int32 = 0
    Private pConfiguration As TerrariaServerConfigurationElement = Nothing
#End Region

#Region " Public Events "
    Public Event StandardErrorOccured(ByVal ErrorText As String)
    Public Event StandardOutputOccured(ByVal OutputText As String)
#End Region

#Region " Constructors "

    'default constructor
    Public Sub New()
        MyBase.New()
    End Sub

#End Region

#Region " Public Methods "

    ''' <summary>
    ''' Starts the server using the supplied configuration
    ''' </summary>
    ''' <param name="serverConfiguration">Server's configuration, including it's location, config file, server commands, etc...</param>
    ''' <returns>
    ''' -1 (true) if sucessful
    ''' 0 if not started (may already be running)
    ''' May return it's exit value is the process failed to start
    ''' </returns>
    ''' <remarks></remarks>
    Public Function StartServer(ByVal serverConfiguration As TerrariaServerConfigurationElement) As Int32
        Dim returnValue As Int32

        If Not pProcessStarted Then
            pServerProcess = New Process

            'set configuration as a private member
            pConfiguration = serverConfiguration

            'configure process
            Me.ConfigureProcess()

            Try
                'Start process
                returnValue = pServerProcess.Start()
                pProcessStarted = True

                'Start listening for process events
                AddHandler pServerProcess.OutputDataReceived, AddressOf ProcessOutputHandler
                AddHandler pServerProcess.ErrorDataReceived, AddressOf ProcessErrorHandler
                pServerProcess.BeginErrorReadLine()
                pServerProcess.BeginOutputReadLine()
            Catch ex As Exception
                'set return value to exit code
                If Not IsNothing(pServerProcess) Then
                    returnValue = pServerProcess.ExitCode
                End If

                're-throw exception
                Throw
            End Try
        Else
            'Process already started
            returnValue = 0
        End If

        Return returnValue
    End Function

    ''' <summary>
    ''' Stops the Terraria server if it's already running
    ''' </summary>
    ''' <returns>Returns the process Exit code</returns>
    ''' <remarks></remarks>
    Public Function StopServer() As Int32
        Dim returnValue As Int32

        'stop process only if it's started
        If pProcessStarted Then
            'stop the process elegantly
            Me.SendCommand(pConfiguration.ShutdownCommand)
            pServerProcess.WaitForExit()

            'get the exit code
            returnValue = pServerProcess.ExitCode

            'dispose of the process
            pServerProcess.Close()
            pServerProcess = Nothing
            pProcessStarted = 0
        Else
            returnValue = 0
        End If

        Return returnValue
    End Function

    ''' <summary>
    ''' Sends a command to the Terraria server
    ''' </summary>
    ''' <param name="commandText">text of the command itself</param>
    ''' <remarks>No error checking is done on syntax or existence of the actual command.</remarks>
    Public Sub SendCommand(ByVal commandText As String)
        If Not pProcessStarted Then
            'Server not started
            Throw New Exception(String.Format(EX_COMMAND_SERVER_NOT_STARTED, commandText))
        Else
            'Send command via standard input
            Dim inputWriter As StreamWriter = pServerProcess.StandardInput
            inputWriter.WriteLine(commandText)
            inputWriter.Flush()
        End If

    End Sub

    ''' <summary>
    ''' Broadcasts a message to all players currently online
    ''' </summary>
    ''' <param name="broadcastMessage">The text to be broadcasted</param>
    ''' <remarks></remarks>
    Public Sub Broadcast(ByVal broadcastMessage As String)
        Me.SendCommand(String.Format(pConfiguration.BroadcastCommand, broadcastMessage))
    End Sub

    ''' <summary>
    ''' Immediately saves the world
    ''' </summary>
    ''' <remarks>Autosave ONLY if allowed. Some servers such as TShock have auto-save built-in and it would be redundant to autosave again.</remarks>
    Public Sub AutoSave()
        If Me.pConfiguration.AllowAutoSave Then
            Me.SendCommand(pConfiguration.SaveCommand)
        End If
    End Sub

    ''' <summary>
    ''' Reads and clears the content of the console output buffer
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function ReadOutputBuffer() As String
        Dim returnValue As String

        'read and clear the buffer
        returnValue = pConsoleOutputBuffer.ToString
        pConsoleOutputBuffer.Clear()

        Return returnValue
    End Function

    ''' <summary>
    ''' Reads and clears the content of the error buffer
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function ReadErrorBuffer() As String
        Dim returnValue As String

        'read and clear the buffer
        returnValue = pErrorBuffer.ToString
        pErrorBuffer.Clear()

        Return returnValue
    End Function

    ''' <summary>
    ''' Clears the error buffer. For example if you stop buffering errors; the buffer will not clear automatically.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub ClearErrorBuffer()
        pErrorBuffer.Clear()
    End Sub

    ''' <summary>
    ''' Clears the output buffer. For example if you stop buffering outputs; the buffer will not clear automatically.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub ClearOutputBuffer()
        pConsoleOutputBuffer.Clear()
    End Sub

    ''' <summary>
    ''' Activates the logging of errors to a text file.
    ''' </summary>
    ''' <param name="path">Full Path and file name of the error log. If the file does not exist, a file will be created. Folder MUST exist.</param>
    ''' <param name="OverwriteFile">Tells to overwrite or append to a file if already existing.</param>
    ''' <remarks></remarks>
    Public Sub LogErrorsToFile(ByVal path As String, Optional ByVal OverwriteFile As Boolean = False)
        'close stream if already in use
        'ie:save file and open a new one with new (or same) filename
        If Not IsNothing(pErrorStream) Then
            pErrorStream.Flush()
            pErrorStream.Close()
            pErrorStream = Nothing
        End If

        'open a new file only if a path is supplied
        If Not String.IsNullOrEmpty(path.Trim) Then
            pErrorStream = New StreamWriter(path, (Not OverwriteFile))
            pErrorStream.AutoFlush = True
        End If
    End Sub

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <param name="path">Full Path and file name of the console output log. If the file does not exist, a file will be created. Folder MUST exist.</param>
    ''' <param name="OverwriteFile">Tells to overwrite or append to a file if already existing</param>
    ''' <remarks></remarks>
    Public Sub LogOutputToFile(ByVal path As String, Optional ByVal OverwriteFile As Boolean = False)
        'close stream if already in use
        'ie:save file and open a new one with new (or same) filename
        If Not IsNothing(pConsoleOutputStream) Then
            pConsoleOutputStream.Flush()
            pConsoleOutputStream.Close()
            pConsoleOutputStream = Nothing
        End If

        'open a new file only if a path is supplied
        If Not String.IsNullOrEmpty(path.Trim) Then
            pConsoleOutputStream = New StreamWriter(path, (Not OverwriteFile))
            pConsoleOutputStream.AutoFlush = True
        End If
    End Sub

    ''' <summary>
    ''' Force Error and Output (if any) logs to flush.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub FlushLogs()
        'flush output
        If Not IsNothing(pConsoleOutputStream) Then
            pConsoleOutputStream.Flush()
        End If

        'flush error
        If Not IsNothing(pErrorStream) Then
            pErrorStream.Flush()
        End If
    End Sub

    ''' <summary>
    ''' TShock specfic command. Select world on startup menu
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub SelectTShockWorld()
        If Me.IsTShock Then
            'only select world if server is TShock
            Me.SendCommand(Me.pConfiguration.TShockWorld)
        End If
    End Sub

    ''' <summary>
    ''' TShock specific command. Select port on startup menu
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub SelectTShockPort()
        If Me.IsTShock Then
            'only select port if server is TShock
            Me.SendCommand(Me.pConfiguration.TShockPort)
        End If
    End Sub

#End Region

#Region " Public Properties "

    ''' <summary>
    ''' Tells the class to buffer or not errors from this point forward. Previously occured errors will not be in buffer and buffer will not be cleared on stop.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property BufferErrors() As Boolean
        Get
            Return pBufferErrors
        End Get
        Set(ByVal value As Boolean)
            pBufferErrors = value
        End Set
    End Property

    ''' <summary>
    ''' Tells the class to buffer or not outputs from this point forward. Previously occured outputs will not be in buffer and buffer will not be cleared on stop.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property BufferOutput() As Boolean
        Get
            Return pBufferConsoleOutput
        End Get
        Set(ByVal value As Boolean)
            pBufferConsoleOutput = value
        End Set
    End Property

    ''' <summary>
    ''' Indicates the number of errors since the server started
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property ErrorCount As Int32
        Get
            Return pErrorCount
        End Get
    End Property

    ''' <summary>
    ''' Incdicates if the server is currently running
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property IsServerRunning As Boolean
        Get
            Return pProcessStarted
        End Get
    End Property

    ''' <summary>
    ''' Return TRUE if server is configured to run under TShock
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property IsTShock As Boolean
        Get
            Return (IsNumeric(Me.pConfiguration.TShockWorld) And IsNumeric(Me.pConfiguration.TShockPort))
        End Get
    End Property

    ''' <summary>
    ''' Name of the process
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Name As String
        Get
            Return pConfiguration.Name
        End Get
    End Property


#End Region

#Region " Private Methods "

    ''' <summary>
    ''' Do the basic process configuration
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub ConfigureProcess()

        'get physical location of the Assembly
        Dim assemblyFilePath As String
        assemblyFilePath = Me.GetPathPart(System.Reflection.Assembly.GetExecutingAssembly().Location)

        'prep logging before attempting to start
        If pConfiguration.LogErrors Then
            Me.LogErrorsToFile(String.Concat(assemblyFilePath, pConfiguration.Name, FILE_ERROR_LOG_SUFFIX))
        End If
        If pConfiguration.LogConsoleOutput Then
            Me.LogOutputToFile(String.Concat(assemblyFilePath, pConfiguration.Name, FILE_CONSOLE_LOG_SUFFIX))
        End If

        'set buffers
        pBufferErrors = pConfiguration.BufferErrors
        pBufferConsoleOutput = pConfiguration.BufferConsole

        'set process configuration
        pServerProcess.StartInfo.FileName = pConfiguration.Location
        pServerProcess.StartInfo.WorkingDirectory = Me.GetPathPart(pConfiguration.Location)
        pServerProcess.StartInfo.Arguments = pConfiguration.Arguments
        pServerProcess.StartInfo.RedirectStandardInput = True
        pServerProcess.StartInfo.RedirectStandardOutput = True
        pServerProcess.StartInfo.RedirectStandardError = True
        pServerProcess.StartInfo.UseShellExecute = False
        pServerProcess.StartInfo.CreateNoWindow = True
    End Sub

#End Region

#Region " Private Event Handlers "

    ''' <summary>
    ''' Event that is called when the process sends text to stdOut.
    ''' </summary>
    ''' <param name="sendingProcess">A reference to the calling process</param>
    ''' <param name="outputLine">Text sent to stdOut</param>
    ''' <remarks></remarks>
    Private Sub ProcessOutputHandler(sendingProcess As Object, outputLine As DataReceivedEventArgs)
        If Not String.IsNullOrEmpty(outputLine.Data) Then
            'append CRLF
            Dim outText As String = String.Concat(outputLine.Data, Environment.NewLine)

            'write to buffer
            If pBufferConsoleOutput Then
                pConsoleOutputBuffer.Append(outText)
            End If

            'write to stream
            If Not IsNothing(pConsoleOutputStream) Then
                pConsoleOutputStream.Write(outText)
            End If

            'raise event
            RaiseEvent StandardOutputOccured(outText)
        End If
    End Sub

    ''' <summary>
    ''' Event that is called when the process sends an error message to stdErr.
    ''' </summary>
    ''' <param name="sendingProcess">A reference to the calling process</param>
    ''' <param name="errorLine">Text sent to stdErr</param>
    ''' <remarks></remarks>
    Private Sub ProcessErrorHandler(sendingProcess As Object, errorLine As DataReceivedEventArgs)
        If Not String.IsNullOrEmpty(errorLine.Data) Then
            'increment error count
            pErrorCount += 1

            'append CRLF
            Dim errText As String = String.Concat(errorLine.Data, Environment.NewLine)

            'write to buffer
            If pBufferErrors Then
                pErrorBuffer.Append(errText)
            End If

            'write to stream
            If Not IsNothing(pErrorStream) Then
                pErrorStream.Write(errText)
            End If

            'raise event
            RaiseEvent StandardErrorOccured(errText)
        End If
    End Sub

    ''' <summary>
    ''' Utility function to retreive the path part of a file
    ''' </summary>
    ''' <param name="fullPath">Full path to extract path from</param>
    ''' <returns>Full path ending in "\"</returns>
    ''' <remarks></remarks>
    Private Function GetPathPart(ByVal fullPath As String) As String
        Dim someFileInfo As FileInfo
        Dim someFilePath As String

        someFileInfo = New FileInfo(fullPath)
        someFilePath = someFileInfo.Directory.FullName

        If Not someFilePath.EndsWith("\") Then
            someFilePath = String.Concat(someFilePath, "\")
        End If

        Return someFilePath
    End Function


#End Region

End Class